﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Win32;
using System.Runtime.InteropServices;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Imaging;
using System.Reflection;
using System.Globalization;
using System.IO;
using System.Management;
using System.Net;
using System.IO.Compression;
using System.Web;
using System.Collections.Specialized;
using System.Threading;

namespace Skugo.Shared
{
    public enum CrashInfo
    {
        TimeZone,
        Region,
        Os,
        Osx64,
        Appx64,
        Processors,
        Processor,
        Manufacturer,
        Model,
        Version,
        Machine,
        UserName,
        Domain,
        SystemRunTime,
        AppRunTime,
        MemoryWorkingSet,
        MemoryUsed,
        CommandLine,
        CurrentDirectory,
        SystemDirectory,
        AppVersion,
        AppName,
        AppProduct,
        CustomVersion,
        CustomData,
        Message,
        Stack,
        ExceptionSource,
        ExceptionSourceMethod,
        Log,
        Assemblies,
        Screenshot,
        Dump,
    }

    public class CrashHandler : IDisposable
    {
        /// <summary>
        /// If the application has its own custom scheme for versioning, this will be printed with the report.
        /// </summary>
        public String CustomVersion = null;

        /// <summary>
        /// Extra data printed with each report.
        /// </summary>
        public String CustomData = null;

        /// <summary>
        /// The url that we submit the report to.
        /// </summary>
        public String Url = @"http://www.skugo.org/crash.php";

        private Stopwatch Timer = new Stopwatch();

        private CrashDialog Dialog;

        private Boolean AlreadyHit = false;


        public CrashHandler()
        {
            this.Timer.Start();
            AppDomain.CurrentDomain.UnhandledException += this.OnUnhandledException;
        }

        private static String ParenthesisIfDifferent(String primary, String secondary)
        {
            if (primary != secondary)
            {
                return primary + " (" + secondary + ")";
            }
            else
            {
                return primary;
            }
        }

        public void GetSystemInfo(Dictionary<CrashInfo, String> results)
        {
            try { results.Add(CrashInfo.TimeZone, TimeZone.CurrentTimeZone.StandardName); } catch { }
            try { results.Add(CrashInfo.Region, ParenthesisIfDifferent(CultureInfo.CurrentCulture.DisplayName, CultureInfo.CurrentCulture.EnglishName)); } catch { }
            try { results.Add(CrashInfo.Os, Environment.OSVersion.ToString()); } catch {}
            try { results.Add(CrashInfo.Machine, Environment.MachineName); } catch {}
            try { results.Add(CrashInfo.Osx64, Environment.Is64BitOperatingSystem.ToString()); } catch {}
            try { results.Add(CrashInfo.Appx64, Environment.Is64BitProcess.ToString()); } catch {}
            try { results.Add(CrashInfo.Processors, Environment.ProcessorCount.ToString()); } catch {}
            try { results.Add(CrashInfo.Version, Environment.Version.ToString()); } catch {}
            try { results.Add(CrashInfo.UserName, Environment.UserName); } catch {}
            try { results.Add(CrashInfo.Domain, Environment.UserDomainName); } catch {}
            try { results.Add(CrashInfo.SystemRunTime, (Environment.TickCount / 1000.0).ToString()); } catch {}
            try { results.Add(CrashInfo.AppRunTime, this.Timer.Elapsed.TotalSeconds.ToString()); } catch {}
            try { results.Add(CrashInfo.MemoryWorkingSet, Environment.WorkingSet.ToString()); } catch {}
            try { results.Add(CrashInfo.MemoryUsed, GC.GetTotalMemory(false).ToString()); } catch {}
            try { results.Add(CrashInfo.CommandLine, Environment.CommandLine); } catch {}
            try { results.Add(CrashInfo.CurrentDirectory, Environment.CurrentDirectory); } catch {}
            try { results.Add(CrashInfo.SystemDirectory, Environment.SystemDirectory); } catch {}

            if (this.CustomVersion != null)
            {
                try { results.Add(CrashInfo.CustomVersion, this.CustomVersion); } catch {}
            }
            if (this.CustomData != null)
            {
                try { results.Add(CrashInfo.CustomData, this.CustomData); } catch { }
            }

            try
            {
                Assembly assembly = Assembly.GetEntryAssembly();
                var appInfo = FileVersionInfo.GetVersionInfo(assembly.Location);
                results.Add(CrashInfo.AppVersion, appInfo.FileVersion);
                results.Add(CrashInfo.AppName, appInfo.FileName);
                results.Add(CrashInfo.AppProduct, appInfo.ProductName);
            }
            catch
            {
            }

            try
            {
                RegistryKey key = Registry.LocalMachine;
                key = key.OpenSubKey("HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0");
                var processorName = (String)key.GetValue("ProcessorNameString");
                results.Add(CrashInfo.Processor, processorName);
            }
            catch
            {
            }

            try
            {
                ManagementClass mc = new ManagementClass("Win32_ComputerSystem");
                foreach (ManagementObject mo in mc.GetInstances())
                {
                    results.Add(CrashInfo.Manufacturer, mo["Manufacturer"].ToString());
                    results.Add(CrashInfo.Model, mo["Model"].ToString());
                }
            }
            catch
            {
            }
        }

        private static void AppendKeyValue(StringBuilder builder, String key, Object value)
        {
            builder.AppendLine(key + ":");
            builder.AppendLine("  " + value);
        }

        public void Dispose()
        {
            AppDomain.CurrentDomain.UnhandledException -= this.OnUnhandledException;
        }

        public void SubmitHandledCrashReport(Exception e)
        {
            this.OnUnhandledException(this, new UnhandledExceptionEventArgs(e, false));
        }

        private void OnUnhandledException(object sender, UnhandledExceptionEventArgs e)
        {
            // We don't want to receive exceptions twice
            if (this.AlreadyHit)
                return;

            this.AlreadyHit = true;

            // If we're in the debugger, don't bother
            if (Debugger.IsAttached)
                return;

            // Holds all the information we want to report about this crash
            var map = new Dictionary<CrashInfo, String>();

            // Generate a file name for the mini-dump
            var dumpFile = Path.GetTempFileName() + ".mdmp"; ;

            // We want to do the crash report as soon as possible (on the same thread the crash occurred on!)
            try
            {
                // Attempt to save out the mini-dump
                using (FileStream fs = new FileStream(dumpFile, FileMode.Create, FileAccess.ReadWrite, FileShare.Write))
                {
                    MiniDump.Write(fs.SafeFileHandle, MiniDump.Option.Normal, MiniDump.ExceptionInfo.Present);
                }
            }
            catch
            {
            }

            // Now we're going to try and do some stuff on another thread, because this stuff could take a while
            ThreadStart threadFunction = () =>
            {
                // Ideally, if the mini-dump was saved out, we can now open it, zip it, and save it as text
                try
                {
                    var ms = new MemoryStream();
                    var gzip = new GZipStream(ms, CompressionMode.Compress);

                    using (var read = File.Open(dumpFile, FileMode.Open, FileAccess.Read))
                    {
                        var buffer = new Byte[4096];
                        for (; ; )
                        {
                            var readBytes = read.Read(buffer, 0, 4096);

                            if (readBytes == 0 || read.CanRead == false)
                            {
                                break;
                            }

                            gzip.Write(buffer, 0, readBytes);
                        }
                    }

                    var data = ms.ToArray();
                    var miniDumpText = Convert.ToBase64String(data);
                    miniDumpText = WebUtility.HtmlEncode(miniDumpText);
                    map.Add(CrashInfo.Dump, miniDumpText);
                }
                catch
                {
                }

                // After doing the memory dump, we want to take a screenshot (as soon after it happened, especially if the crash is on another thread...)
                try
                {
                    var ms = new MemoryStream();

                    // Attempt to take a screenshot
                    var screenshot = ScreenCapture.CaptureWindow(Process.GetCurrentProcess().MainWindowHandle);
                    screenshot.Save(ms, ImageFormat.Jpeg);
                    var data = ms.ToArray();
                    var screenshotText = Convert.ToBase64String(data);
                    screenshotText = WebUtility.HtmlEncode(screenshotText);
                    map.Add(CrashInfo.Screenshot, screenshotText);
                }
                catch
                {
                }

                // Get system information
                try
                {
                    GetSystemInfo(map);
                }
                catch
                {
                }

                // Get direct information from the exception
                try
                {
                    // Attempt to get the exception object as a C# exception
                    var exception = e.ExceptionObject as Exception;

                    // If this is a standard C# exception...
                    if (exception != null)
                    {
                        // The message will incude all the inner exceptions
                        var message = new StringBuilder();
                        message.AppendLine(exception.Message);
                        var inner = exception.InnerException;
                        while (inner != null)
                        {
                            message.AppendLine(inner.ToString());
                        }

                        // Add information about the message, source, etc
                        map.Add(CrashInfo.Message, message.ToString());
                        map.Add(CrashInfo.ExceptionSource, exception.Source);
                        map.Add(CrashInfo.ExceptionSourceMethod, exception.TargetSite.ToString());

                        // Now we need to write out the stack trace for the exception
                        map.Add(CrashInfo.Stack, exception.StackTrace);
                    }
                    else
                    {
                        // We don't really know what data is in this exception, so just stringify it
                        map.Add(CrashInfo.Message, e.ExceptionObject.ToString());

                        // Write out the stack trace (may not be correct...)
                        map.Add(CrashInfo.Stack, Environment.StackTrace);
                    }
                }
                catch
                {
                }

                // Get any loaded assemblies (lets us know what plugins are loaded, etc)
                try
                {
                    // Make a list of loaded assemblies
                    var assemblies = new StringBuilder();

                    // Loop through all loaded assemblies
                    foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
                    {
                        // Add the assembly name so we can see if any extra plugins are loaded
                        assemblies.AppendLine(assembly.GetName().Name);
                    }

                    // Write out which assemblies we have loaded
                    map.Add(CrashInfo.Assemblies, assemblies.ToString());
                }
                catch
                {
                }

                // Upload the values to an online database
                using (var client = new WebClient())
                {
                    var values = new NameValueCollection();

                    foreach (var pair in map)
                    {
                        values.Add(pair.Key.ToString(), pair.Value);
                    }

                    var result = client.UploadValues(this.Url, values);
                    var text = System.Text.Encoding.Default.GetString(result);
                }
            };

            // If the application is terminated, gracefully kill it without bringing up the crash dialog
            if (e.IsTerminating)
            {
                // We're terminating anyways, so just directly run the thread function
                threadFunction();

                // Attempt many ways of killing it
                Environment.Exit(-1);
                Process.GetCurrentProcess().Kill();
            }
            else
            {
                // We're not terminating the application, so do a bunch of stuff on another thread so we don't stall the current thread
                Thread thread = new Thread(threadFunction);
                // We want to fully complete sending the crash report before the application is allowed to close...
                thread.IsBackground = false;
                thread.Start();
            }
        }

    }


    public static class MiniDump
    {
        // Taken almost verbatim from http://blog.kalmbach-software.de/2008/12/13/writing-minidumps-in-c/
        [Flags]
        public enum Option : uint
        {
            // From dbghelp.h:
            Normal = 0x00000000,
            WithDataSegs = 0x00000001,
            WithFullMemory = 0x00000002,
            WithHandleData = 0x00000004,
            FilterMemory = 0x00000008,
            ScanMemory = 0x00000010,
            WithUnloadedModules = 0x00000020,
            WithIndirectlyReferencedMemory = 0x00000040,
            FilterModulePaths = 0x00000080,
            WithProcessThreadData = 0x00000100,
            WithPrivateReadWriteMemory = 0x00000200,
            WithoutOptionalData = 0x00000400,
            WithFullMemoryInfo = 0x00000800,
            WithThreadInfo = 0x00001000,
            WithCodeSegs = 0x00002000,
            WithoutAuxiliaryState = 0x00004000,
            WithFullAuxiliaryState = 0x00008000,
            WithPrivateWriteCopyMemory = 0x00010000,
            IgnoreInaccessibleMemory = 0x00020000,
            ValidTypeFlags = 0x0003ffff,
        };

        public enum ExceptionInfo
        {
            None,
            Present
        }

        //typedef struct _MINIDUMP_EXCEPTION_INFORMATION {
        //    DWORD ThreadId;
        //    PEXCEPTION_POINTERS ExceptionPointers;
        //    BOOL ClientPointers;
        //} MINIDUMP_EXCEPTION_INFORMATION, *PMINIDUMP_EXCEPTION_INFORMATION;
        [StructLayout(LayoutKind.Sequential, Pack = 4)]  // Pack=4 is important! So it works also for x64!
        public struct MiniDumpExceptionInformation
        {
            public uint ThreadId;
            public IntPtr ExceptionPointers;
            [MarshalAs(UnmanagedType.Bool)]
            public bool ClientPointers;
        }

        //BOOL
        //WINAPI
        //MiniDumpWriteDump(
        //    __in HANDLE hProcess,
        //    __in DWORD ProcessId,
        //    __in HANDLE hFile,
        //    __in MINIDUMP_TYPE DumpType,
        //    __in_opt PMINIDUMP_EXCEPTION_INFORMATION ExceptionParam,
        //    __in_opt PMINIDUMP_USER_STREAM_INFORMATION UserStreamParam,
        //    __in_opt PMINIDUMP_CALLBACK_INFORMATION CallbackParam
        //    );

        // Overload requiring MiniDumpExceptionInformation
        [DllImport("dbghelp.dll", EntryPoint = "MiniDumpWriteDump", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)]
        static extern bool MiniDumpWriteDump(IntPtr hProcess, uint processId, SafeHandle hFile, uint dumpType, ref MiniDumpExceptionInformation expParam, IntPtr userStreamParam, IntPtr callbackParam);

        // Overload supporting MiniDumpExceptionInformation == NULL
        [DllImport("dbghelp.dll", EntryPoint = "MiniDumpWriteDump", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)]
        static extern bool MiniDumpWriteDump(IntPtr hProcess, uint processId, SafeHandle hFile, uint dumpType, IntPtr expParam, IntPtr userStreamParam, IntPtr callbackParam);

        [DllImport("kernel32.dll", EntryPoint = "GetCurrentThreadId", ExactSpelling = true)]
        static extern uint GetCurrentThreadId();

        public static bool Write(SafeHandle fileHandle, Option options, ExceptionInfo exceptionInfo)
        {
            Process currentProcess = Process.GetCurrentProcess();
            IntPtr currentProcessHandle = currentProcess.Handle;
            uint currentProcessId = (uint)currentProcess.Id;
            MiniDumpExceptionInformation exp;
            exp.ThreadId = GetCurrentThreadId();
            exp.ClientPointers = false;
            exp.ExceptionPointers = IntPtr.Zero;
            if (exceptionInfo == ExceptionInfo.Present)
            {
                exp.ExceptionPointers = System.Runtime.InteropServices.Marshal.GetExceptionPointers();
            }
            bool bRet = false;
            if (exp.ExceptionPointers == IntPtr.Zero)
            {
                bRet = MiniDumpWriteDump(currentProcessHandle, currentProcessId, fileHandle, (uint)options, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
            }
            else
            {
                bRet = MiniDumpWriteDump(currentProcessHandle, currentProcessId, fileHandle, (uint)options, ref exp, IntPtr.Zero, IntPtr.Zero);
            }
            return bRet;
        }

        public static bool Write(SafeHandle fileHandle, Option dumpType)
        {
            return Write(fileHandle, dumpType, ExceptionInfo.None);
        }
    }

    /// <summary>
    /// Provides functions to capture the entire screen, or a particular window, and save it to a file.
    /// </summary>
    public static class ScreenCapture
    {
        /// <summary>
        /// Creates an Image object containing a screen shot of the entire desktop
        /// </summary>
        /// <returns></returns>
        public static Bitmap CaptureScreen()
        {
            return CaptureWindow( User32.GetDesktopWindow() );
        }
        /// <summary>
        /// Creates an Image object containing a screen shot of a specific window
        /// </summary>
        /// <param name="handle">The handle to the window. (In windows forms, this is obtained by the Handle property)</param>
        /// <returns></returns>
        public static Bitmap CaptureWindow(IntPtr handle)
        {
            // get te hDC of the target window
            IntPtr hdcSrc = User32.GetWindowDC(handle);
            // get the size
            User32.RECT windowRect = new User32.RECT();
            User32.GetWindowRect(handle,ref windowRect);
            int width = windowRect.right - windowRect.left;
            int height = windowRect.bottom - windowRect.top;
            // create a device context we can copy to
            IntPtr hdcDest = GDI32.CreateCompatibleDC(hdcSrc);
            // create a bitmap we can copy it to,
            // using GetDeviceCaps to get the width/height
            IntPtr hBitmap = GDI32.CreateCompatibleBitmap(hdcSrc,width,height);
            // select the bitmap object
            IntPtr hOld = GDI32.SelectObject(hdcDest,hBitmap);
            // bitblt over
            GDI32.BitBlt(hdcDest,0,0,width,height,hdcSrc,0,0,GDI32.SRCCOPY);
            // restore selection
            GDI32.SelectObject(hdcDest,hOld);
            // clean up
            GDI32.DeleteDC(hdcDest);
            User32.ReleaseDC(handle,hdcSrc);
            // get a .NET image object for it
            Bitmap img = Image.FromHbitmap(hBitmap);
            // free up the Bitmap object
            GDI32.DeleteObject(hBitmap);
            return img;
        }
        /// <summary>
        /// Captures a screen shot of a specific window, and saves it to a file
        /// </summary>
        /// <param name="handle"></param>
        /// <param name="filename"></param>
        /// <param name="format"></param>
        public static void CaptureWindowToFile(IntPtr handle, string filename, ImageFormat format)
        {
            Image img = CaptureWindow(handle);
            img.Save(filename,format);
        }
        /// <summary>
        /// Captures a screen shot of the entire desktop, and saves it to a file
        /// </summary>
        /// <param name="filename"></param>
        /// <param name="format"></param>
        public static void CaptureScreenToFile(string filename, ImageFormat format)
        {
            Image img = CaptureScreen();
            img.Save(filename,format);
        }
     
        /// <summary>
        /// Helper class containing Gdi32 API functions
        /// </summary>
        private class GDI32
        {
           
            public const int SRCCOPY = 0x00CC0020; // BitBlt dwRop parameter
            [DllImport("gdi32.dll")]
            public static extern bool BitBlt(IntPtr hObject,int nXDest,int nYDest,
                int nWidth,int nHeight,IntPtr hObjectSource,
                int nXSrc,int nYSrc,int dwRop);
            [DllImport("gdi32.dll")]
            public static extern IntPtr CreateCompatibleBitmap(IntPtr hDC,int nWidth,
                int nHeight);
            [DllImport("gdi32.dll")]
            public static extern IntPtr CreateCompatibleDC(IntPtr hDC);
            [DllImport("gdi32.dll")]
            public static extern bool DeleteDC(IntPtr hDC);
            [DllImport("gdi32.dll")]
            public static extern bool DeleteObject(IntPtr hObject);
            [DllImport("gdi32.dll")]
            public static extern IntPtr SelectObject(IntPtr hDC,IntPtr hObject);
        }

        /// <summary>
        /// Helper class containing User32 API functions
        /// </summary>
        private class User32
        {
            [StructLayout(LayoutKind.Sequential)]
            public struct RECT
            {
                public int left;
                public int top;
                public int right;
                public int bottom;
            }
            [DllImport("user32.dll")]
            public static extern IntPtr GetDesktopWindow();
            [DllImport("user32.dll")]
            public static extern IntPtr GetWindowDC(IntPtr hWnd);
            [DllImport("user32.dll")]
            public static extern IntPtr ReleaseDC(IntPtr hWnd,IntPtr hDC);
            [DllImport("user32.dll")]
            public static extern IntPtr GetWindowRect(IntPtr hWnd,ref RECT rect);
        }
    }
}
